// 在开头引入所需的类型和库
// 注意点：
// 1. 如果我引入的类是作为类型来使用的话，我需要使用 import type 来引入，并且引入类型有两种写法
// 一种是import type {这里面写多个类型} from '模块路径' 
// 另一种是import {type 类型1, 类型2} from '模块路径' 这种写法可以和普通引入混合使用
// 2. 我引入的类如果是默认导出的，我需要使用 import DefaultType from '模块路径' 来引入，这里的名字可以自己取
// 3. 我引入的类如果是命名导出的，我需要使用 import { NamedType } from '模块路径' 来引入，
// 这里面的名字必须和我导出的名字一致
// 4. 如果我引入的类是作为值来使用的话，我需要使用普通的 import 来引入
// 5. 如果既作为类型又作为值使用：使用普通 import 即可
import type { 
  AxiosInstance, 
  InternalAxiosRequestConfig, 
  AxiosResponse, 
  Method,
  AxiosRequestHeaders
} from 'axios';
import axios from 'axios';
import { ElMessage, ElLoading } from 'element-plus';
import router from '@/router';
// 引入Pinia的auth store
import { useAuthStore } from '@/stores/authStore';

// 定义API响应的基础结构
//1.
//泛型接口需要类型参数 ： ApiResponse<T> 定义时声明了一个类型参数 T ，
// 用来约束 data 字段的类型。在使用这个接口时，
// 按照TypeScript的严格要求， 必须为这个泛型参数提供具体的类型值 
// 有两种方式提供
// 1. 直接在接口定义的时候指定类型参数也就是默认的参数类型，
// 如果我在后面不自己指定那么就默认是这个类型，我这里使用any类型表示任意类型都可以
// 2. 在使用的时候显示指定类型参数，
// 比如我在后面使用了这个接口那么我就可以在使用的时候显示的使用<类型>来指定类型
interface ApiResponse<T = any> {
  code: number;
  message: string;
  data: T;
  //索引签名允许您定义一个接口，
  // 该接口 不仅包含明确定义的属性，还可以接受任意数量的额外属性 。
  //需要注意 明确定义的属性类型必须与索引签名的值类型兼容，
  // 就比如这里我前面三个属性的类型都是number string any 
  // 都可以兼容索引签名的值类型any
  //我这里的key的名字是任意的，但是我的key的类型必须是string类型
  [key: string]: any; // 允许其他额外字段
}

// 定义请求配置：继承高版本Axios的核心配置类型
//这里是对我axios当中我要传入的请求类的扩展
//同时每个属性都加上了可选符? 表示这个属性是可选的
// 用来表示我是否要显示加载动画 是否要忽略错误提示 是否要重试
interface RequestConfig extends InternalAxiosRequestConfig {
  showLoading?: boolean;  // 是否显示加载动画
  ignoreError?: boolean;  // 是否忽略错误提示
  _retry?: boolean;       // 标记是否已重试，用于防止无限循环刷新令牌
  //就是说这个变量表示我的刷新令牌是否已经在刷新中或者刷新过了
}

// 创建Axios实例，对我的axios进行了全局的配置
//只要是我在后面是使用这个axios实例 那么我就会使用这个全局的配置
const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_BASE_API, // 使用环境变量配置基础URL
  timeout: 10000,// 超时时间10秒
  withCredentials: true  // 允许跨域请求携带Cookie，重要：用于刷新令牌
});

// 加载实例管理
//- 1.
//首先通过 typeof ElLoading.service 获取到 ElLoading.service 这个函数的完整类型定义
//- 2.
//然后通过 ReturnType<> 从这个函数类型中提取出它的返回值类型
//- 3.
//最后结合 | null 表示变量可以是加载动画实例或者 null
//// 所以下面两个类型声明是等价的
// let user1: ReturnType<typeof getUser>; 通过函数获取函数的返回值是什么
// let user2: { id: number; name: string; }; 直接将函数返回值的类型赋值给他
let loadingInstance: ReturnType<typeof ElLoading.service> | null;
let requestCount = 0;//这个是记录我有几个加载动画 但是只会显示一个

// 显示加载动画
const showLoading = () => {
  //当我调用这个方法的时候表示我要显示加载动画，但是我可能现在已经存在了加载动画
  // 所以我需要判断一下如果当前没有加载动画实例if (requestCount === 0) 
  // 如果没有那么我就创建一个新的加载动画实例
  if (requestCount === 0) {
    loadingInstance = ElLoading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.1)'
    });
  }
  //加载动画数量加一，因为我可能会有多个请求同时发送，
  // 所以我需要记录一下我有几个加载动画，只有当我所有的加载都完成了
  // 我才会隐藏加载动画
  requestCount++;
};

// 隐藏加载动画
//调用这个函数表示我的请求完成了该请求对应的加载动画不需要了
// 所以我需要将加载动画数量减一
// 如果当前加载动画数量小于等于0 那么我就隐藏加载动画
// 并将加载动画实例设置为null 并将加载动画数量设置为0
const hideLoading = () => {
  requestCount--;
  if (requestCount <= 0) {
    //我的loadingInstance 是我前面加载函数的返回值，也可以把它理解成我的这个加载动画的实例
    // 它里面包含了close方法可以关闭我的加载动画
    //这里加上了? 表示如果loadingInstance 是null 那么我就不调用close方法
    //因为如果我不调用showLoading 那么我就不会创建加载动画实例
    // 所以我这里加上? 表示如果loadingInstance 是null 那么我就不调用close方法
    loadingInstance?.close();
//  - 释放引用资源 ：将 loadingInstance 设置为 null 会 
// 断开对加载动画实例的引用 ，
// 这样 JavaScript 的垃圾回收机制就可以回收这个不再被引用的实例对象占用的内存
// - 防止内存泄漏 ：如果不将其设置为 null ，即使动画已经关闭，
// 变量仍会持有对实例的引用，可能导致 内存泄漏
    loadingInstance = null;
//     - 状态同步 ：与 loadingInstance = null; 配合，
// 确保计数器状态与加载动画实例状态保持同步
// - 边界情况处理 ：防止在异常情况下（如网络请求失败、被取消等）
// 导致计数器值变为负数或不准确
    requestCount = 0;
  }
  //在我最后关闭什么资源的时候
  // 最好是将和他相关的所有资源如对象设置为空普通变量重置为初始值
  //可以释放资源，重置资源 同时避免特殊情况的发生
};

// 请求拦截器
//对我所有使用这个axios实例的请求都进行拦截处理进行一下的操作
//第一个参数：请求成功拦截器函数
//- 参数 ： config - 请求配置对象，包含了请求的所有信息（URL、方法、请求头、请求体等）
//- 返回值 ：在成功拦截器中：
// - 可以直接返回 config 对象（请求配置）
// - 可以返回一个 resolve 为配置对象的 Promise
//第二个参数：请求失败拦截器函数（可选）
// - 参数 ： error - 请求错误对象
// - 返回值 ：可以返回 任何类型的 Promise 对象 ，但 Promise 的内容会影响后续行为：
// - 如果返回 Promise.reject(error) ：请求会失败，错误会被传递
// - 如果返回 Promise.resolve(config) ：使用新配置继续请求
// - 如果返回 Promise.resolve(response) ：直接返回模拟响应，不再发送请求
service.interceptors.request.use(
  //为什么这里可以不规范参数的类型，
  // 是因为我这里是调用我axios库当中的方法他已经对我的参数类型进行了规范了
  (config) => {
    //注意这里我对我的config进行了类型断言，
    // 是因为我这里是调用我axios库当中的方法他已经对我的参数类型进行了规范了
    // 但是我这里的config是我自己对其属性做了一个扩展后面我需要使用到这些扩展属性，
    // 所以我需要对他进行类型断言
    const reqConfig = config as RequestConfig;
    
    // 显示加载动画
    // 注意当我的配置当中不存在showLoading属性时候
    // 我的reqConfig.showLoading是undefined而false确实不等于undefined
    // 所以默认就是加载动画
    if (reqConfig.showLoading !== false) {
      showLoading();
    }

    // 从Pinia中获取访问令牌并添加到请求头
    const authStore = useAuthStore();
    if (authStore.accessToken) {
//       - headers 是配置对象中的请求头集合
// - Authorization 是一个标准的 HTTP 请求头字段，用于向服务器提供身份验证凭据
// - Bearer 是认证方案类型，表示使用的是令牌认证机制
// - ${authStore.accessToken} 是在字符串中嵌入变量值的语法
// - 整体格式遵循 HTTP 标准， Bearer 和令牌之间用空格分隔
      reqConfig.headers.Authorization = `Bearer ${authStore.accessToken}`;
    }

    // 添加公共请求参数 如时间戳防止缓存
    if (reqConfig.method?.toUpperCase() === 'GET') {
      reqConfig.params = {
        // ...reqConfig.params 是将 reqConfig.params 中的所有属性展开到新对象中
        // 这是一种对象合并的方式，用于将多个对象的属性合并到一个新对象中
        // 这里的作用是将 reqConfig.params 中的所有属性展开到新对象中
        // 并将 _t: Date.now() 这个属性添加到新对象中
        // 整体效果是将 reqConfig.params 中的所有属性和 _t: Date.now() 合并到一个新对象中
        ...reqConfig.params,
        _t: Date.now()  // 时间戳防止缓存
      };
    }
    //最后将我的新的配置对象返回，发送请求
    return reqConfig;
  },
  (error) => {
    hideLoading();
    ElMessage.error(`请求配置错误: ${error.message}`);
    //  Promise变为拒绝状态 并返回错误，请求不在发送，使调用者知道请求失败
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
 (response: AxiosResponse<ApiResponse>) => {
  hideLoading();
  // 从响应中提取数据部分 注意这里我使用的是解构赋值的方式提取数据部分
  // 这是一种对象解构的方式，用于从对象中提取属性值并赋值给变量
  // 这里的作用是从响应对象中提取 data 属性值并赋值给变量 data
  // 整体效果是将响应对象中的 data 属性值提取出来，赋值给变量 data
//   - 对于基本类型（如字符串、数字）的属性，解构出来的 name 和 age 仍然是 普通的 JavaScript 变量 ，不具有响应式
// - 对于引用类型（如对象、数组）的属性，解构出来的仍然是一个 Proxy 对象， 保持响应式特性
//   当你进行解构赋值时，本质上是在调用属性的 getter 方法获取其当前值，然后将这个值赋给新的变量。这个过程会导致：

// - 对于基本类型，你得到的是一个值的副本
// - 对于引用类型，你得到的是对象的引用（Vue 3 中是 Proxy 引用）
  const { data } = response;
  const reqConfig = response.config as RequestConfig; // response.config 是请求配置的「快照」

  // 处理业务错误（后端业务逻辑返回的错误）注意这里我使用的是!=而不是！== 因为我的后端可能返回的是字符串类型的200
  if (data.code != 200) {
    // 注意：此处不再处理业务状态码401，因为令牌过期判断由后端框架自动完成
    // 后端框架会直接返回HTTP 401状态码，由响应拦截器的错误处理部分处理

    // 其他业务错误状态码处理
    if (!reqConfig.ignoreError) {//如果配置了忽略错误提示，就不显示
      // 显示后端返回的业务错误信息
    ElMessage.error(data.message || '操作失败');
    }
    // 将业务错误 reject 出去，方便上层代码捕获处理
    return Promise.reject(new Error(data.message || '接口返回错误'));
  }
  //如果我后端返回的消息不是空字符串或者undefined 就显示成功消息
  //对于后端如果我返回的对象当中没有message属性，那么我这个属性就是undefined
  if(data.message != undefined && data.message != ""){
    ElMessage.success(data.message);
  }
  // 业务处理成功，返回响应数据也就是我后端封装的对象
  //为什么我这里可以返回data.data是因为我的data.data类型是any可以适配任何的返回类型
//   ，Axios 期望响应拦截器的成功回调函数返回以下类型之一：
// - AxiosResponse<any, any, {}> 对象
// - 解析为 AxiosResponse<any, any, {}> 对象的 Promise
  return data.data;
},
//这里使用了async 以下的这个函数对于我的返回值我会自动包装成一个Promise对象
  async (error) => {
//对于我写代码，我可以将我整个代码当中存在的东西写在纸上，
// 当我要设计或者要完成一个函数的时候就看看我的这个函数需不需要处理纸上写的东西，不记录下来就很容易忽略掉一些元素
    hideLoading();

    // 处理HTTP错误状态码
    // 401 令牌过期或无效时触发这个401不是我返回的对象当中的状态码而是我的http请求对应的状态码
    //这个状态码是由我的springsecurity框架自动返回的
    if (error.response?.status === 401) {
      // 尝试刷新令牌
      //handleTokenExpiration这个函数的返回值是一个Promise对象
      //我这里没有加await是因为我需要这个请求完成之后才进行之后的代码处理
      return handleTokenExpiration(error.config);
    }

    // 处理其他网络错误
    let errorMessage = '网络请求失败，请稍后重试';
    
    if (error.response) {
      switch (error.response.status) {
        case 403:
          errorMessage = '没有权限访问该资源';
          break;
        case 404:
          errorMessage = '请求的资源不存在';
          break;
        case 500:
          errorMessage = '服务器内部错误';
          break;
        case 502:
          errorMessage = '网关错误';
          break;
        case 504:
          errorMessage = '服务器响应超时';
          break;
        default:
          errorMessage = `请求错误 (${error.response.status})`;
      }
// error.request 存在，表示：
//  1.
//    请求已经成功发送到服务器
//  2.
//    但 没有收到任何响应 （服务器没有返回任何数据）
//  3.
//    这通常意味着网络连接问题或服务器无响应
    } else if (error.request) {
      errorMessage = '请求无响应';
    }

    ElMessage.error(errorMessage);
    return Promise.reject(error);
  }
);

/**
 * 处理令牌过期逻辑
 * @param config 请求配置
 */
//这里是我自己定义函数 我的参数需要写类型
const handleTokenExpiration = async (config: InternalAxiosRequestConfig) => {
  if (!config) {
    return Promise.reject(new Error('请求配置不存在'));
  }

  const reqConfig = config as RequestConfig;
  
  //这个是如果我的同一个请求已经尝试过一次刷新令牌了，那么我就直接拒绝这个请求
  // 这是为了防止一个请求循环重复刷新令牌
  if (reqConfig._retry) {
    // 清除用户状态并跳转到登录页
    const authStore = useAuthStore();
    authStore.clearAuth();
    
    router.replace({
      path: '/login',
      query: {
        redirect: router.currentRoute.value.fullPath//记录当前所在的页面 后面登录成功之后可以直接返回
      }
    });
    
    ElMessage.error('登录已过期，请重新登录');
    return Promise.reject(new Error('登录已过期'));
  }

  // 标记为已重试,这个是防止一个请求循环重复刷新令牌，我将这个标记放在请求配置里，
  // 对于同一个请求来说，这个标记是共享的
  reqConfig._retry = true;
  
  try {
    const authStore = useAuthStore();
    // 调用Pinia中的刷新令牌方法
    // 刷新令牌存储在HttpOnly Cookie中，由浏览器自动携带
    //使用了 await 关键字，这会等待 Promise 状态变为 resolved 并获取其返回值。
    // 所以只要 refreshToken() 内部正确 resolve 出字符串类型的令牌，这里的 newAccessToken 就会是字符串类型
    const newAccessToken = await authStore.refreshToken();
    
    // 使用新令牌重新设置请求头
    if (newAccessToken && reqConfig.headers) {
      reqConfig.headers.Authorization = `Bearer ${newAccessToken}`;//这个请求头是规定的
    }
    
    // 重新发送原请求
    return service(reqConfig);
  } catch (error) {
    // 刷新令牌也过期，需要重新登录
    router.replace({
      path: '/login',
      query: {
        redirect: router.currentRoute.value.fullPath
      }
    });
    return Promise.reject(new Error('登录已过期，请重新登录'));
  }
};

/**
 * 通用请求方法
 * @param method HTTP方法
 * @param url 请求URL
 * @param data 请求数据
 * @param config 额外配置
 * @returns Promise
 */
//函数声明的方式
// / 函数声明式
// function functionName<T1, T2, ...>(
//   param1: Type1,
//   param2?: Type2, // 可选参数
//   param3: Type3 = defaultValue, // 默认参数
//   ...restParams: Type4[] // rest 参数
// ): ReturnType {
//   // 函数体
//   return returnValue;
// }

// // 函数表达式
// const functionName = <T1, T2, ...>(
//   param1: Type1,
//   param2?: Type2,
//   param3: Type3 = defaultValue,
//   ...restParams: Type4[] // rest 参数，收集剩余参数为数组
// ): ReturnType => {
//   // 函数体
//   return returnValue;
// };
const request = <T = any>(
  method: Method,
  url: string,
  data?: any,
  config?: RequestConfig
): Promise<T> => {
  const requestConfig: RequestConfig = {
    method,//我的变量名和我的键名一样就可以只写一个
    url,
    //这里如果我的方法是GET或者DELETE，我就用params来传递参数
    //否则我就用data来传递参数
    [(method.toUpperCase() === 'GET' || method.toUpperCase() === 'DELETE') ? 'params' : 'data']: data,
    headers: { 'Content-Type': 'application/json;charset=utf-8' } as AxiosRequestHeaders,
    ...config 
  };
  //这里是返回一个Promise对象
//   - 成功的请求最终返回的是 data.data （业务数据）
// - 业务错误的请求最终返回的是被拒绝的 Promise
// - 401 错误可能会触发重试，最终返回重试后的结果
// ## 总结
// service(requestConfig) 的返回值确实主要由响应拦截器决定
  return service(requestConfig);
};

/**
 * GET请求
 */
const get = <T = any>(
  url: string,
  params?: any,
  config?: RequestConfig
): Promise<T> => {
  return request<T>('GET', url, params, config);
};

/**
 * POST请求
 */
const post = <T = any>(
  url: string,
  data?: any,
  config?: RequestConfig
): Promise<T> => {
  return request<T>('POST', url, data, config);
};

/**
 * PUT请求
 */
const put = <T = any>(
  url: string,
  data?: any,
  config?: RequestConfig
): Promise<T> => {
  return request<T>('PUT', url, data, config);
};

/**
 * DELETE请求
 */
const del = <T = any>(
  url: string,
  params?: any,
  config?: RequestConfig
): Promise<T> => {
  return request<T>('DELETE', url, params, config);
};

// 导出封装的请求方法和实例
export { service, request, get, post, put, del };
//这里如果导出去的类型就需要使用type来导出
export type { RequestConfig, ApiResponse };
